﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ResourceClient.cs" company="XamlNinja">
//   2011 Richard Griffin and Ollie Riches
// </copyright>
// <summary>
//   The resource client.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace WP7Contrib.Communications
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Browser;
    using System.Reactive.Concurrency;
    using System.Reactive.Disposables;
    using System.Reactive.Linq;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Xml.Linq;
    using System.Xml.Serialization;
    using Compression;
    using Logging;
    using Newtonsoft.Json;

    /// <summary>
    /// The resource client.
    /// </summary>
    public sealed class ResourceClient : IHandleResources
    {
        private class NullResponse { }

        /// <summary>
        /// The invalid url.
        /// </summary>
        private const string InvalidUrl = "Invalid url specified, unable to create Uri instance from value specified, url - '{1}'";

        /// <summary>
        /// The invalid get url.
        /// </summary>
        private const string InvalidGetUrl = "Invalid GET url specified, unable to create Uri instance from value specified, url - '{1}'";

        /// <summary>
        /// The invalid post url.
        /// </summary>
        private const string InvalidPostUrl = "Invalid POST url specified, unable to create Uri instance from value specified, url - '{0}'";

        /// <summary>
        /// The invalid put url.
        /// </summary>
        private const string InvalidPutUrl = "Invalid PUT url specified, unable to create Uri instance from value specified, url - '{0}'";

        /// <summary>
        /// The invalid delete url.
        /// </summary>
        private const string InvalidDeleteUrl = "Invalid DELETE url specified, unable to create Uri instance from value specified, url - '{0}'";

        /// <summary>
        /// The failed to build url.
        /// </summary>
        private const string FailedToBuildUrl = "Failed to build url, baseUrl - '{0}', parameters - '{1}'";

        /// <summary>
        /// The failed serialize.
        /// </summary>
        private const string FailedSerialize = "Failed to serialize resource to {0}.";

        /// <summary>
        /// The failed deserialize.
        /// </summary>
        private const string FailedDeserialize = "Failed to deserialize resource from {0}.";

        /// <summary>
        /// The failed request.
        /// </summary>
        private const string FailedRequest = "Failed to execute HTTP {0} request, message - '{1}'.";

        /// <summary>
        /// The failed response.
        /// </summary>
        private const string FailedResponse = "Failed to process response from execution of HTTP {0} request, message - '{1}'.";

        /// <summary>
        /// The resources using data contract serializer.
        /// </summary>
        private static readonly IDictionary<Type, bool> ResourcesUsingDataContractSerializer = new Dictionary<Type, bool>();

        /// <summary>
        /// The sync object to make accessing the data contract serializer dictionary thread safe.
        /// </summary>
        private readonly object sync = new object();

        /// <summary>
        /// The logger used to capture diagnostics information.
        /// </summary>
        private readonly ILog log;

        /// <summary>
        /// The templated get url.
        /// </summary>
        private string templatedGetUrl;

        /// <summary>
        /// The templated post url.
        /// </summary>
        private string templatedPostUrl;

        /// <summary>
        /// The templated put url.
        /// </summary>
        private string templatedPutUrl;

        /// <summary>
        /// The templated delete url.
        /// </summary>
        private string templatedDeleteUrl;

        /// <summary>
        /// The serializer namespaces.
        /// </summary>
        private XmlSerializerNamespaces serializerNamespaces;

        /// <summary>
        /// Defines how json serializer will handle missing properties etc.
        /// </summary>
        private JsonHandlerDefinitions jsonHandlerDefinitions = JsonHandlerDefinitions.Default;

        /// <summary>
        /// The username used for authenticating requests.
        /// </summary>
        private string username;

        /// The password used for authenticating requests.
        private string password;

        /// GZip compressions is requested.
        private bool gzip;

        /// <summary>
        ///  The custom headers
        /// </summary>
        private readonly IDictionary<string, IHeader> headers;

        /// <summary>
        /// Initializes a new instance of the <see cref="ResourceClient"/> class.
        /// </summary>
        public ResourceClient() : this(new DebugLog())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResourceClient"/> class.
        /// </summary>
        /// <param name="log">
        /// The logger used to capture diagnostics information.
        /// </param>
        public ResourceClient(ILog log)
        {
            this.log = log;
            this.headers = new Dictionary<string, IHeader>();
        }

        /// <summary>
        /// Gets the resource type.
        /// </summary>
        public ResourceType Type { get; private set; }

        /// <summary>
        /// Defines the resource type to be used for the requests.
        /// </summary>
        /// <param name="type">
        /// The resource type being used - json, xml, soap.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources ForType(ResourceType type)
        {
            this.Type = type;

            return this;
        }

        /// <summary>
        /// Defines the url to be used for request operation types - GET, POST, PUT and DELETE
        /// </summary>
        /// <param name="url">
        /// The url endpoint for acessing the resource, this can contain string format characters '{0}' which can be subsituted
        /// when the operation is executed.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources UseUrl(string url)
        {
            ThrowIfUrlIsInvalid(url, InvalidUrl);
            
            this.templatedGetUrl = url;
            this.templatedPostUrl = url;
            this.templatedPutUrl = url;
            this.templatedDeleteUrl = url;

            return this;
        }

        /// <summary>
        /// The url to use for GET operation,  this can contain string format characters '{0}' which are substituted
        /// when the operation is executed with the @param value(s).
        /// </summary>
        /// <param name="getUrl">
        /// The get url.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources UseUrlForGet(string getUrl)
        {
            ThrowIfUrlIsInvalid(getUrl, InvalidGetUrl);
            this.templatedGetUrl = getUrl;
            
            return this;
        }

        /// <summary>
        /// The url to use for POST operation, this can contain string format characters '{0}' which are substituted
        /// when the operation is executed with the @param value(s).
        /// </summary>
        /// <param name="postUrl">
        /// The post url.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources UseUrlForPost(string postUrl)
        {
            ThrowIfUrlIsInvalid(postUrl, InvalidPostUrl);
            this.templatedPostUrl = postUrl;

            return this;
        }

        /// <summary>
        /// The url to use for PUT operation, this can contain string format characters '{0}' which are substituted
        /// when the operation is executed with the @param value(s).
        /// </summary>
        /// <param name="putUrl">
        /// The put url.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources UseUrlForPut(string putUrl)
        {
            ThrowIfUrlIsInvalid(putUrl, InvalidPutUrl);
            this.templatedPutUrl = putUrl;

            return this;
        }

        /// <summary>
        /// The url to use for DELETE operation, this can contain string format characters '{0}' which are substituted
        /// when the operation is executed with the @param value(s).
        /// </summary>
        /// <param name="deleteUrl">
        /// The delete url.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources UseUrlForDelete(string deleteUrl)
        {
            ThrowIfUrlIsInvalid(deleteUrl, InvalidDeleteUrl);
            this.templatedDeleteUrl = deleteUrl;

            return this;
        }

        /// <summary>
        /// The with namespace associated with the resource - usually used with SOAP operations.
        /// </summary>
        /// <param name="prefix">
        /// The prefix.
        /// </param>
        /// <param name="namespace">
        /// The namespace to supplied for accessing the resource.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources WithNamespace(string prefix, string @namespace)
        {
            if (this.serializerNamespaces == null)
            {
                this.serializerNamespaces = new XmlSerializerNamespaces();
            }

            this.serializerNamespaces.Add(prefix, @namespace);

            return this;
        }

        public IHandleResources WithGzip()
        {
            this.gzip = true;
            return this;
        }

        /// <summary>
        ///  Allows the setting of how the json serializer will handle common serialization problems such
        /// as missing methods, reference loops etc.
        /// </summary>
        /// <param name="definitions">The defintions</param>
        /// <returns>Returns the instance of the interface - fluent interface style.</returns>
        public IHandleResources WithHandlerDefinitions(JsonHandlerDefinitions definitions)
        {
            this.jsonHandlerDefinitions = definitions;

            return this;
        }

        /// <summary>
        /// Allows the setting of custom headers, many custom headers can be defined, but only one per 'Name' (property on the interface).
        /// </summary>
        /// <param name="header">The custom header.</param>
        /// <returns>Returns the instance of the interface - fluent interface style.</returns>
        public IHandleResources WithHeader(IHeader header)
        {
            if (this.headers.ContainsKey(header.Name))
            {
                this.headers[header.Name] = header;
            }
            else
            {
                this.headers.Add(header.Name, header);
            }
            
            return this;
        }

        /// <summary>
        /// The basic authentication used to access the resource.
        /// </summary>
        /// <param name="user">
        /// The username.
        /// </param>
        /// <param name="pwd">
        /// The password.
        /// </param>
        /// <returns>
        /// Returns the instance of the interface - fluent interface style.
        /// </returns>
        public IHandleResources WithBasicAuthentication(string user, string pwd)
        {
            this.username = user;
            this.password = pwd;

            return this;
        }

        /// <summary>
        /// Executes a GET operation.
        /// </summary>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Get<TResponse>()
            where TResponse : class
        {
            return this.Get<TResponse>(null);
        }

        /// <summary>
        /// Executes a GET operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Get<TResponse>(params object[] @params)
             where TResponse : class
        {
            return this.ExecuteRequestWithNoBody<TResponse>(this.templatedGetUrl, "GET", @params);
        }

        /// <summary>
        /// Executes a PUT operation.
        /// </summary>
        /// <param name="resource">
        /// The resource to be sent with the request.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Put<TRequest, TResponse>(TRequest resource)
            where TRequest : class
            where TResponse : class
        {
            return this.Put<TRequest, TResponse>(resource, null);
        }

        /// <summary>
        /// Executes a PUT operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        /// <param name="resource">
        /// The resource to be sent with the request.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Put<TRequest, TResponse>(TRequest resource, params object[] @params)
            where TRequest : class
            where TResponse : class
        {
            return this.ExecuteRequestWithBody<TRequest, TResponse>(this.templatedPutUrl, "PUT", resource, @params);
        }

        /// <summary>
        /// Executes a POST operation.
        /// </summary>
        /// <param name="resource">
        /// The resource to be sent with the request.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Post<TRequest, TResponse>(TRequest resource)
            where TRequest : class
            where TResponse : class
        {
            return this.Post<TRequest, TResponse>(resource, null);
        }

        /// <summary>
        /// Executes a POST operation.
        /// </summary>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Post<TResponse>()
            where TResponse : class
        {
            return this.Post<TResponse>(null);
        }

        /// <summary>
        /// Executes a POST operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        /// <param name="resource">
        /// The resource to be sent with the request.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Post<TRequest, TResponse>(TRequest resource, params object[] @params)
            where TRequest : class
            where TResponse : class
        {
            return this.ExecuteRequestWithBody<TRequest, TResponse>(this.templatedPostUrl, "POST", resource, @params);
        }

        /// <summary>
        /// Executes a POST operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Post<TResponse>(params object[] @params)
            where TResponse : class
        {
            return this.ExecuteRequestWithNoBody<TResponse>(this.templatedPostUrl, "POST", @params);
        }

        /// <summary>
        /// Executes a DELETE operation.
        /// </summary>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Delete<TResponse>()
            where TResponse : class
        {
            return this.ExecuteRequestWithNoBody<TResponse>(this.templatedDeleteUrl, "DELETE", null);
        }

        /// <summary>
        /// Executes a DELETE operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        public IObservable<TResponse> Delete<TResponse>(params object[] @params)
            where TResponse : class
        {
            return this.ExecuteRequestWithNoBody<TResponse>(this.templatedDeleteUrl, "DELETE", @params);
        }

        /// <summary>
        /// Executes a DELETE operation.
        /// </summary>
        public void Delete()
        {
            this.ExecuteRequestWithNoBody<NullResponse>(this.templatedDeleteUrl, "DELETE", null);
        }

        /// <summary>
        /// Executes a DELETE operation.
        /// </summary>
        /// <param name="params">
        /// The parameters required for the operation - theses will be combined into the url using a string format.
        /// </param>
        public void Delete(params object[] @params)
        {
            this.ExecuteRequestWithNoBody<NullResponse>(this.templatedDeleteUrl, "DELETE", @params);
        }

        /// <summary>
        /// Executes a request which does not contain a body and returns response, e.g. a GET operation.
        /// </summary>
        /// <param name="urlTemplate">
        /// The templated url which is combined with @params values.
        /// </param>
        /// <param name="method">
        /// The operation being executed.
        /// </param>
        /// <param name="params">
        /// The params.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        /// <exception cref="ResourceClientException">
        /// Any exception that occurs is wrapped into a resource client exception.
        /// </exception>
        private IObservable<TResponse> ExecuteRequestWithNoBody<TResponse>(string urlTemplate, string method, IEnumerable<object> @params)
            where TResponse : class
        {
            try
            {
                return Observable.Defer(
                    () => Observable.Create<TResponse>(obs =>
                    {
                        var disposable = new BooleanDisposable();

                        try
                        {
                            var url = this.BuildUrl(@params, urlTemplate);
                            this.log.Write("ResourceClient: Request Url - " + url.AbsoluteUri);
                            var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(url);
                            request.Method = method;
                            request.Accept = this.GetHttpType();
                            request.CookieContainer = new CookieContainer();

                            if (!string.IsNullOrEmpty(this.username))
                            {
                                request.Credentials = new NetworkCredential(this.username, this.password);
                            }

                            this.AddHeaders<object>(request, null, url);

                            Observable.FromAsyncPattern(request.BeginGetResponse, ar =>
                            {
                                try
                                {
                                    var response = this.ProcessResponse<TResponse>(disposable, method, ar, request);

                                    if (disposable.IsDisposed)
                                    {
                                        return;
                                    }

                                    obs.OnNext(response);
                                    obs.OnCompleted();
                                }
                                catch (Exception exn)
                                {
                                    var message = string.Format(FailedRequest, method, exn.Message);
                                    obs.OnError(new ResourceClientException(message, exn));
                                }
                            })();
                        }
                        catch (ResourceClientStatusException exn)
                        {
                           obs.OnError(exn);
                        }
                        catch (Exception exn)
                        {
                            var message = string.Format(FailedRequest, method, exn.Message);
                            obs.OnError(new ResourceClientException(message, exn));
                        }
                        
                        return disposable;
                    })
                );
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRequest, method, exn.Message);
                throw new ResourceClientException(message, exn);
            }
        }

        /// <summary>
        /// Executes a request which does contain a body and returns response, e.g. a POST, PUT operation.
        /// </summary>
        /// <param name="urlTemplate">
        /// The templated url which is combined with @params values.
        /// </param>
        /// <param name="method">
        /// The operation being executed.
        /// </param>
        /// <param name="requestResource">
        /// The request resource, will be included as the body.
        /// </param>
        /// <param name="params">
        /// The params.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        /// <exception cref="ResourceClientException">
        /// Any exception that occurs is wrapped into a resource client exception.
        /// </exception>
        private IObservable<TResponse> ExecuteRequestWithBody<TRequest, TResponse>(string urlTemplate, string method, TRequest requestResource, IEnumerable<object> @params)
            where TRequest : class
            where TResponse : class
        {
            try
            {
                return Observable.Defer(() =>
                {
                    var url = this.BuildUrl(@params, urlTemplate);
                    this.log.Write("ResourceClient: Request Url - " + url.AbsoluteUri);
                    var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(url);
                    var type = this.GetHttpType();
                    request.Method = method;
                    request.Accept = type;
                    request.ContentType = type;
                    request.CookieContainer = new CookieContainer();

                    if (!string.IsNullOrEmpty(this.username))
                    {
                        request.Credentials = new NetworkCredential(this.username, this.password);
                    }

                    this.AddHeaders<object>(request, null, url);
                
                    var data = this.Serialize(requestResource);

                    return Observable.FromAsyncPattern<Stream>(request.BeginGetRequestStream, request.EndGetRequestStream)()
                        .ObserveOn(Scheduler.ThreadPool)
                        .SelectMany(stream =>
                        {
                            this.log.Write("ResourceClient: Writing to Request Stream");
                            using (stream)
                            {
                                stream.Write(data, 0, data.Length);
                                stream.Close();
                            }

                            return Observable.FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse)()
                                .ObserveOn(Scheduler.ThreadPool);
                       }, (stream, response) => this.ProcessResponse<TResponse>(method, (HttpWebResponse)response))
                            .ObserveOn(Scheduler.ThreadPool);
                });
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedRequest, method, exn.Message);
                throw new ResourceClientException(message, exn);
            }
        }

        /// <summary>
        /// Process the HTTP response body into a response resource.
        /// </summary>
        /// <param name="disposable">The current disposable observable</param>
        /// <param name="method">
        /// The operation that has been executed.
        /// </param>
        /// <param name="asyncResult">
        /// The async result.
        /// </param>
        /// <param name="request">
        /// The HTTP web request.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        /// <exception cref="ResourceClientStatusException">
        /// If the response status is not 200 then a status exception is thrown.
        /// </exception>
        /// <exception cref="ResourceClientException">
        /// Any exception that occurs is wrapped into a resource client exception.
        /// </exception>
        private TResponse ProcessResponse<TResponse>(BooleanDisposable disposable, string method, IAsyncResult asyncResult, HttpWebRequest request)
            where TResponse : class
        {
            if (disposable.IsDisposed)
            {
                 request.Abort();
                 return null;
            }

            this.log.Write("ResourceClient: ProcessResponse, method -  " + method);

            var started = DateTime.Now;
            try
            {
                using (var response = (HttpWebResponse)request.EndGetResponse(asyncResult))
                {
                    this.log.Write("ResourceClient: Response length -  {0}", response.ContentLength);

                    this.LogResponseHeaders(response);
                    using (var stream = ExtractStream(response))
                    {
                        if (typeof (TResponse) == typeof (XDocument) || typeof (TResponse) == typeof (XElement))
                        {
                            using (var reader = new StreamReader(stream))
                            {
                                var content = reader.ReadToEnd();
                                reader.Dispose();

                                this.log.Write("ResourceClient: Xml content length - {0}", content.Length);

                                return typeof (TResponse) == typeof (XDocument)
                                           ? XDocument.Load(new StringReader(content)) as TResponse
                                           : XElement.Load(new StringReader(content)) as TResponse;
                            }
                        }

                        if (this.Type == ResourceType.Json)
                            return this.DeserializeJson<TResponse>(stream);
                        else if (this.Type == ResourceType.Xml || this.Type == ResourceType.SoapXml)
                            return this.DeserializeXml<TResponse>(stream);
                        else return null;
                    }
                }
            }
            catch (WebException webExn)
            {
                using (var response = (HttpWebResponse)webExn.Response)
                {
                    var status = new Status((int)response.StatusCode, response.StatusDescription);
                    throw new ResourceClientStatusException(status, webExn);
                }
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedResponse, method, exn.Message);
                throw new ResourceClientException(message, exn);
            }
            finally
            {
                var finished = DateTime.Now;
                this.log.Write("ResourceClient: Deserialization time - {0} ms", (finished - started).TotalMilliseconds);
            }
        }
        
        /// <summary>
        /// Process the HTTP response body into a response resource.
        /// </summary>
        /// <param name="method">
        /// The operation that has been executed.
        /// </param>
        /// <param name="response">
        /// The HTTP web response.
        /// </param>
        /// <returns>
        /// Returns an observable response resource.
        /// </returns>
        /// <exception cref="ResourceClientException">
        /// Any exception that occurs is wrapped into a resource client exception.
        /// </exception>
        private TResponse ProcessResponse<TResponse>(string method, HttpWebResponse response)
            where TResponse : class
        {
            this.log.Write("ResourceClient: ProcessResponse, method -  " + method);

            var started = DateTime.Now;
            try
            {
                this.LogResponseHeaders(response);
                this.log.Write("ResourceClient: Response length -  {0}", response.ContentLength);

                using(var stream = ExtractStream(response))
                {
                    if (typeof (TResponse) == typeof (XDocument) || typeof (TResponse) == typeof (XElement))
                    {
                        var reader = new StreamReader(stream);
                        var content = reader.ReadToEnd();

                        this.log.Write("ResourceClient: Xml content length - {0}", content.Length);

                        return typeof (TResponse) == typeof (XDocument)
                                   ? XDocument.Load(new StringReader(content)) as TResponse
                                   : XElement.Load(new StringReader(content)) as TResponse;
                    }

                    if (this.Type == ResourceType.Json)
                        return this.DeserializeJson<TResponse>(stream);
                    else if (this.Type == ResourceType.Xml || this.Type == ResourceType.SoapXml)
                        return this.DeserializeXml<TResponse>(stream);
                    else return null;
                }
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedResponse, method, exn.Message);
                this.log.Write("ResourceClient: " + message);
                throw new ResourceClientException(message, exn);
            }
            finally
            {
                var finished = DateTime.Now;
                this.log.Write("ResourceClient: Deserialization time - {0} ms", (finished - started).TotalMilliseconds);
            }
        }

        private static Stream ExtractStream(WebResponse response)
        {
            if (response.Headers[HttpRequestHeader.ContentEncoding] == "gzip")
            {
                using (var stream = new GZipStream(response.GetResponseStream()))
                {
                    var memoryStream = new MemoryStream((int) stream.Length);
                    stream.CopyTo(memoryStream);
                    memoryStream.Seek(0, SeekOrigin.Begin);

                    return memoryStream;
                }
            }

            return response.GetResponseStream();
        }

        /// <summary>
        /// Adds any additional headers that have been declared with the 'WithHeader' or WithGZip headers.
        /// </summary>
        /// <typeparam name="TRequest">The request type.</typeparam>
        /// <param name="request">The HTTP web request.</param>
        /// <param name="requestResource">The request resource (can be unll)</param>
        /// <param name="url">The url.</param>
        private void AddHeaders<TRequest>(HttpWebRequest request, TRequest requestResource, Uri url)
        {
            if (this.gzip)
            {
                request.Headers["accept-encoding"] = "gzip";
            }

            foreach (var keyValue in this.headers)
            {
                var header = keyValue.Key;
                var value = keyValue.Value.Value(requestResource, url);

                if (header.ToLower() == "content-type")
                {
                    request.ContentType = value;
                }
                else if (header.ToLower() == "accept")
                {
                    request.Accept = value;
                }
                else
                {
                    request.Headers[keyValue.Key] = keyValue.Value.Value(requestResource, url);
                }
            }

            this.LogRequestHeaders(request);
        }

        /// <summary>
        /// Logs all request headers
        /// </summary>
        /// <param name="request">The request.</param>
        private void LogRequestHeaders(WebRequest request)
        {
            foreach (var key in request.Headers.AllKeys)
            {
                this.log.Write("ResourceClient: Request header, name - '{0}', value - '{1}'", key, request.Headers[key]);
            }
        }

        /// <summary>
        /// Logs all response headers
        /// </summary>
        /// <param name="response">The request response.</param>
        private void LogResponseHeaders(WebResponse response)
        {
            foreach (var key in response.Headers.AllKeys)
            {
                this.log.Write("ResourceClient: Response header, name - '{0}', value - '{1}'", key, response.Headers[key]);
            }
        }

        /// <summary>
        /// Determines the MIME type for the resource type.
        /// </summary>
        /// <returns>
        /// Returns the MIME type for the resource type.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// Unknown resource type.
        /// </exception>
        private string GetHttpType()
        {
            if (this.Type == ResourceType.Xml)
            {
                return "application/xml";
            }

            if (this.Type == ResourceType.Json)
            {
                return "application/json";
            }

            if (this.Type == ResourceType.SoapXml)
            {
                return "application/soap+xml";
            }

            throw new ArgumentException("Failed to determine HTTP type!");
        }

        /// <summary>
        /// Build the formatted url from the base url and the supplied parameters
        /// </summary>
        /// <param name="params">
        /// The params.
        /// </param>
        /// <param name="baseUrl">
        /// The base url.
        /// </param>
        /// <returns>
        /// Returns the formatted url as Uri class.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// Throws an argument exception if it fails to build the url.
        /// </exception>
        private Uri BuildUrl(IEnumerable<object> @params, string baseUrl)
        {
            try
            {
                string url;

                if (@params == null || @params.Count() == 0)
                {
                    return new Uri(baseUrl);
                }

                if (!baseUrl.Contains("{0}"))
                {
                    url = baseUrl + "/{0}";
                }
                else
                {
                    url = baseUrl;
                }

                return new Uri(string.Format(url, @params.ToArray()));
            }
            catch (Exception exn)
            {
                var parameters = @params != null ? string.Join(",", @params.Cast<string>().ToArray()) : string.Empty;
                var message = string.Format(FailedToBuildUrl, baseUrl, parameters);

                this.log.Write(message);
                throw new ArgumentException(message, exn);
            }
        }

        /// <summary>
        /// Deserializes the response stream as JSON.
        /// </summary>
        /// <param name="stream">
        /// The response stream.
        /// </param>
        /// <returns>
        /// Returns the deserialized response as JSON.
        /// </returns>
        /// <exception cref="Exception">
        /// Any deserialization errors are returned as an exeption.
        /// </exception>
        private TResponse DeserializeJson<TResponse>(Stream stream)
            where TResponse : class
        {
            try
            {
                TResponse result;
                using (var streamReader = new StreamReader(stream))
                using (JsonReader jsonReader = new JsonTextReader(streamReader))
                {
                    if (streamReader.BaseStream.Length == 0)
                    {
                        return null;
                    }

                    var serializer = new JsonSerializer
                    {
                        NullValueHandling = this.jsonHandlerDefinitions.NullValueHandling,
                        MissingMemberHandling = this.jsonHandlerDefinitions.MissingMemberHandling,
                        ReferenceLoopHandling = this.jsonHandlerDefinitions.ReferenceLoopHandling
                    };
                    result = (TResponse)serializer.Deserialize(jsonReader, typeof(TResponse));
                    jsonReader.Close();
                }

                return result;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedDeserialize, "json");
                this.log.Write(message);
                throw new Exception(message, exn);
            }
        }

        /// <summary>
        /// Deserializes the response stream as XML.
        /// </summary>
        /// <param name="stream">
        /// The response stream.
        /// </param>
        /// <returns>
        /// Returns the deserialized response as XML.
        /// </returns>
        /// <exception cref="Exception">
        /// Any deserialization errors are returned as an exeption.
        /// </exception>
        private TResponse DeserializeXml<TResponse>(Stream stream)
            where TResponse : class
        {
            if (stream.Length == 0)
            {
                return null;
            }

            if (this.HasDataContractAttribute(typeof(TResponse)))
            {
                try
                {
                    var serializer = new DataContractSerializer(typeof(TResponse));
                    return (TResponse)serializer.ReadObject(stream);
                }
                catch (Exception exn)
                {
                    this.log.Write(exn.Message);
                }
            }
            else
            {
                try
                {
                    var serializer = new XmlSerializer(typeof(TResponse));
                    return (TResponse)serializer.Deserialize(stream);
                }
                catch (Exception exn)
                {
                    this.log.Write(exn.Message);
                }
            }

            var message = string.Format(FailedDeserialize, "xml");
            this.log.Write(message);
            throw new Exception(message);
        }

        /// <summary>
        /// Serializes the request resource as a byte array.
        /// </summary>
        /// <param name="resource">
        /// The request resource.
        /// </param>
        /// <returns>
        /// Returns the serialized request as a byte array.
        /// </returns>
        /// <exception cref="Exception">
        /// Any serialization errors are returned as an exeption.
        /// </exception>
        private byte[] Serialize<TRequest>(TRequest resource)
            where TRequest : class
        {
            var started = DateTime.Now;
            try
            {
                return this.Type == ResourceType.Json ? this.SerializeJson(resource) : this.SerializeXml(resource);
            }
            finally
            {
                var finished = DateTime.Now;
                this.log.Write("ResourceClient: Serialization time - {0} ms", (finished - started).TotalMilliseconds);
            }
        }

        /// <summary>
        /// Serializes the request resource as an JSON byte array.
        /// </summary>
        /// <param name="resource">
        /// The request resource.
        /// </param>
        /// <returns>
        /// Returns the serialized request as an JSON byte array.
        /// </returns>
        /// <exception cref="Exception">
        /// Any serialization errors are returned as an exeption.
        /// </exception>
        private byte[] SerializeJson<TRequest>(TRequest resource)
            where TRequest : class
        {
            try
            {
                using (var sw = new StringWriter())
                {
                    var serializer = new JsonSerializer
                    {
                        NullValueHandling = this.jsonHandlerDefinitions.NullValueHandling,
                        MissingMemberHandling = this.jsonHandlerDefinitions.MissingMemberHandling,
                        ReferenceLoopHandling = this.jsonHandlerDefinitions.ReferenceLoopHandling
                    };
                    serializer.Serialize(sw, resource);

                    return Encoding.UTF8.GetBytes(sw.ToString());
                }
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedSerialize, "json");
                this.log.Write(message);
                throw new Exception(message, exn);
            }
        }

        /// <summary>
        /// Serializes the request resource as an XML byte array.
        /// </summary>
        /// <param name="resource">
        /// The request resource.
        /// </param>
        /// <returns>
        /// Returns the serialized request as an XML byte array.
        /// </returns>
        /// <exception cref="Exception">
        /// Any serialization errors are returned as an exeption.
        /// </exception>
        private byte[] SerializeXml<TRequest>(TRequest resource)
            where TRequest : class
        {
            if (typeof(TRequest) == typeof(XDocument) || typeof(TRequest) == typeof(XElement))
            {
                return Encoding.UTF8.GetBytes(resource.ToString());
            }

            if (this.HasDataContractAttribute(typeof(TRequest)))
            {
                try
                {
                    using (var ms = new MemoryStream(4096))
                    {
                        var serializer = new DataContractSerializer(typeof(TRequest));
                        serializer.WriteObject(ms, resource);

                        var data = new byte[ms.Length];
                        ms.Seek(0, SeekOrigin.Begin);
                        ms.Read(data, 0, (int)ms.Length);

                        return data;
                    }
                }
                catch (Exception exn)
                {
                    this.log.Write(exn.Message);
                }
            }
            else
            {
                try
                {
                    using (var ms = new MemoryStream(4096))
                    {
                        var serializer = new XmlSerializer(typeof(TRequest));
                        if (this.serializerNamespaces == null)
                        {
                            serializer.Serialize(ms, resource);
                        }
                        else
                        {
                            serializer.Serialize(ms, resource, this.serializerNamespaces);
                        }

                        var data = new byte[ms.Length];
                        ms.Seek(0, SeekOrigin.Begin);
                        ms.Read(data, 0, (int)ms.Length);

                        return data;
                    }
                }
                catch (Exception exn)
                {
                    this.log.Write(exn.Message);
                }
            }

            var message = string.Format(FailedSerialize, "xml");
            this.log.Write(message);
            throw new Exception(message);
        }

        /// <summary>
        /// Determines if the resource type supports the DataContractSerializer - if it is then use the DataContractSerializer
        /// else use the XmlSerializer.
        /// </summary>
        /// <param name="type">
        /// The type of the resource
        /// </param>
        /// <returns>
        /// Returns true if the type has the DataContractSerializer attribute.
        /// </returns>
        private bool HasDataContractAttribute(Type type)
        {
            if (ResourcesUsingDataContractSerializer.ContainsKey(type))
            {
                return ResourcesUsingDataContractSerializer[type];
            }

            lock (this.sync)
            {
                if (ResourcesUsingDataContractSerializer.ContainsKey(type))
                {
                    return ResourcesUsingDataContractSerializer[type];
                }

                var attributes = type.GetCustomAttributes(false);
                var attribute = attributes.Where(a => a.GetType() == typeof(DataContractAttribute)).FirstOrDefault();

                ResourcesUsingDataContractSerializer.Add(type, attribute != null);
                return attribute != null;
            }
        }

        /// <summary>
        /// Throws an exception if url is invalid.
        /// </summary>
        /// <param name="url">
        /// The url as a string to be validated.
        /// </param>
        /// <param name="failureMessage">
        /// The failure message used for any exception that is thrown.
        /// </param>
        /// <exception cref="ArgumentException">
        /// Throws an exception if it can validate the url.
        /// </exception>
        private static void ThrowIfUrlIsInvalid(string url, string failureMessage)
        {
            if (string.IsNullOrEmpty(url))
            {
                return;
            }

            Uri result;
            if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out result))
            {
                throw new ArgumentException(string.Format(failureMessage, url));
            }
        }
    }
}

